Borland Delphi Informant Magazine Home 1stClass Components


Member Services
FREE Trial Issue
New Subscription
Renew Subscription
Delphi CD-ROM
Report Problems
Change of Address


Delphi Informant
Features
Case Studies
News
New Products
Book Reviews
Product Reviews
Opinion
Back Issues

Downloads
Article files
Third-party files
Upload a File

Informant
ICG News
Contact Us
Advertise with Us
Write for Us
The ultimate Web site for C++Builder developers
Delphi Informant Complete Works
 
The ultimate Web site for C++Builder developers

Readers Choice
Awards 2000


 ò

Dynamic Arrays


 ò

Time Travels


 ò

Mediator Pattern


 ò

Run-time ActiveX


 ò

Scriptable Plug-Ins





Tell a friend
about this article!




Inside OP

VCL / Windows Messages / Delphi 1-5

Modifying VCL Behavior

A Practical Example Using Visual Components

To make a visual component behave differently from its defaults, we generally have to create a new component that descends from the original component's class. This article will show how to dynamically change the behavior of a native Delphi visual component without creating a new class.

How is this possible? The secret is to intercept the Windows messages being sent to the control. This can be accomplished by using a TControl property named WindowProc, which essentially points to a component's Windows message event handler.

To demonstrate this technique, we'll create a LinkedLabel component, which will link itself to any TControl and dynamically modify its behavior. TLinkedLabel will descend from TLabel, and will feature four additional published properties:

  • Associate - the companion control whose behavior we'll be modifying.
  • CapsLock - when this Boolean property is True, certain types of associate controls will process lower-case keystrokes as upper case. This doesn't work with all controls, because not all controls respond to the WM_CHAR message in the same way. Testing reveals that Edit, MaskEdit, Memo, and RichEdit controls all respond to the CapsLock property, while ComboBox does not. Obviously, CapsLock will have little or no effect on many other components, such as a Button or CheckBox control.
  • Gap -  the distance between the LinkedLabel and its associate control.
  • OnTop -  this Boolean property determines whether the LinkedLabel will appear to the left of, or on top of, the associate control.

In addition, TLinkedLabel will keep the Enabled and Visible properties of the LinkedLabel and its associate synchronized. It will also maintain a set distance and orientation from the associate control. This means that when you move the LinkedLabel, the associate moves with it, and vice versa.

Let's take a look at the TLinkedLabel class declaration, shown in Figure 1.

unit LinkedLabel;

 

interface

 

uses

  Messages, Classes, Controls, StdCtrls;

 

type

  TLinkedLabel = class(TLabel)

   private

    // The associate control.

    FAssociate:   TControl;

    // Puts FAssociate into all caps mode.

    FCapsLock:    Boolean;     

    // The distance between the label and the associate.

    FGap:         Integer;     

    // True when the label is on top of the associate.

    FOnTop:       Boolean;     

    // Saves the original value of FAssociate.WindowProc.

    FOldWinProc: TWndMethod;  

    // Used to prevent infinite update loops.

    FUpdating:    Boolean;     

   protected

     procedure Adjust(MoveLabel: Boolean);

     procedure SetGap(Value: Integer);

     procedure SetOnTop(Value: Boolean);

     procedure SetAssociate(Value: TControl);

     procedure NewWinProc(var Message: TMessage);

     procedure Notification(AComponent: TComponent;

      Operation: TOperation); override;

     procedure WndProc(var Message: TMessage); override;

   public

     constructor Create(AOwner :TComponent); override;

     destructor Destroy; override;

   published

     property Associate: TControl

       read FAssociate write SetAssociate;

     property CapsLock: Boolean

       read FCapsLock write FCapsLock;

     property Gap: Integer read FGap write SetGap default 8;

     property OnTop: Boolean read FOnTop write SetOnTop;

   end;

Figure 1: The TLinkedLabel class declaration.

Now let's look at the different methods of this component in detail, starting with the constructor. Note that when creating a new object, all of its associated memory is cleared. This will automatically set FAssociate and FOldWinProc to nil, and FCapsLock, FOnTop, and FUpdating to False, all without having to explicitly initialize them in the constructor. Therefore, the only thing we need to set in the constructor is the default Gap value:

implementation

 

constructor TLinkedLabel.Create(AOwner: TComponent);

begin

   inherited;

  FGap := 8;

end;

Now we come to the Adjust method, which is responsible for positioning the LinkedLabel component or the associate control, depending on the value of the MoveLabel parameter. As you'll see in the code, the actual position of the LinkedLabel in relationship to the associate is based on the Gap and OnTop properties (see Figure 2). Although OnTop only provides us with two possible orientations, there are many other possibilities that could easily be programmed into this component. However, adding a lot of "bells and whistles" to TLinkedLabel is not the focus of this article, and has, therefore, been entrusted to the reader.

procedure TLinkedLabel.Adjust(MoveLabel: Boolean);

var

  dx, dy: Integer;

begin

   if (Assigned(FAssociate)) then begin

     if (FOnTop) then

       begin

        dx := 0;

        dy := Height + FGap;

       end

     else

       begin

        dx := Width + FGap;

        dy := (Height - FAssociate.Height) div 2;

       end;

     if (MoveLabel) then

       begin

        Left := FAssociate.Left - dx;

        Top  := FAssociate.Top - dy;

       end

     else

       begin

        FAssociate.Left := Left + dx;

        FAssociate.Top  := Top + dy;

       end;

   end;

end;

Figure 2: The Adjust method.

At this point, we come to the set methods of the Gap and OnTop properties (see Figure 3). These are needed so we can reposition the LinkedLabel when the Gap or OnTop values are modified.

procedure TLinkedLabel.SetGap(Value: Integer);

begin

   if (FGap <> Value) then

     begin

      FGap := Value;

      Adjust(True);

     end;

end;

 

procedure TLinkedLabel.SetOnTop(Value: Boolean);

begin

   if (FOnTop <> Value) then

     begin

      FOnTop := Value;

      Adjust(True);

     end;

end;

Figure 3: The set methods of the Gap and OnTop properties.

Now we come to the SetAssociate method (see Figure 4).

procedure TLinkedLabel.SetAssociate(Value: TControl);

begin

   if (Value <> FAssociate) then begin

     if (Assigned(FAssociate)) then

      FAssociate.WindowProc := FOldWinProc;

    FAssociate := Value;

     if (Assigned(Value)) then

       begin

        Adjust(True);

        Enabled := FAssociate.Enabled;

        Visible := FAssociate.Visible;

         FOldWinProc := FAssociate.WindowProc;

        FAssociate.WindowProc := NewWinProc;

       end;

   end;

end;

Figure 4: The SetAssociate method.

To understand it, we need to discuss the WindowProc property in more detail. WindowProc is defined as of type TWndMethod. TWndMethod can be found in the Controls unit with the following definition:

TWndMethod = procedure(var Message: TMessage) of object;

Notice that FOldWinProc is also defined as a TWndMethod, and that the NewWinProc method has the same parameter structure as TWndMethod. This allows us to point FOldWinProc to the current value of WindowProc, and assign WindowProc to the NewWinProc method.

Why do we need to use FOldWinProc if WindowProc is just another event property? Because the difference between WindowProc and any other event property is that WindowProc is already pointing to an existing event handler. If we simply point WindowProc to our own method, the control will no longer be able to respond to any Windows messages. To solve this problem, we set FOldWinProc to the current value of WindowProc, before pointing WindowProc to the NewWinProc method.

In NewWinProc, we call the old message handler, via FOldWinProc, after and acting upon specific Windows messages. Because we modify the WindowProc property on the associate control, it's important that we restore its former value before changing to a new associate component.

It's also important that we don't leave the associate's WindowProc property pointing to a routine that no longer exists. We therefore call SetAssociate(nil) in the destructor, which, as we've seen, will restore WindowProc to its original value:

destructor TLinkedLabel.Destroy;

begin

  SetAssociate(nil);

   inherited;

end;

In addition, we don't want to be pointing to an associate that no longer exists. By overriding the Notification method, we can know when the associate control is destroyed, and reset our pointer to the associate accordingly:

procedure TLinkedLabel.Notification(AComponent: TComponent;

  Operation: TOperation);

begin

   if ((Operation = opRemove) and

       (AComponent = FAssociate)) then

    SetAssociate(nil);

end;

Now we come to the NewWinProc method (see Figure 5). Here, we simply look for specific Windows messages being sent to the associate component. It's important to realize that although this method is only called by the associate control, it's actually part of the LinkedLabel, i.e. Self = LinkedLabel, not the associate control. This is identical to creating an OnClick event handler for a button. The OnClick event handler is created as part of the button's parent form, and is not a new method extending the TButton class.

procedure TLinkedLabel.NewWinProc(var Message: TMessage);

var

  Ch: Char;

begin

   if (Assigned(FAssociate) and (not FUpdating)) then begin

     FUpdating := True;

     try

       case(Message.Msg) of

        WM_CHAR:

           if (FCapsLock) then begin

            Ch := Char(TWMKey(Message).CharCode);

             if (Ch >= 'a') and (Ch <= 'z') then

              TWMKey(Message).CharCode := ord(UpCase(Ch));

           end;

        CM_ENABLEDCHANGED:

          Enabled := FAssociate.Enabled;

        CM_VISIBLECHANGED:

          Visible := FAssociate.Visible;

        WM_SIZE, WM_MOVE, WM_WINDOWPOSCHANGED:

          Adjust(True);

       end;

     finally

      FUpdating := False;

     end;

   end;

  FOldWinProc(Message);

end;

Figure 5: The NewWinProc method.

If you examine this routine, you'll see we make no attempt to process Windows messages. We react to specific messages, then let the associate process them normally by calling FOldWinProc. In the case of the WM_CHAR message, we change part of the message, causing the component to think an upper-case character was pressed.

Finally, we look at two different messages to see if the associate has been moved. This is because components that descend from TWinControl will get a WM_MOVE message when they're moved, while other visual components (such as a Label) will get the WM_WINDOWPOSCHANGED message. The WM_SIZE message is examined, because if the OnTop property is False, the position of the LinkedLabel will change based on the height of the component.

The last method of our component is where we make adjustments to the associate when the LinkedLabel is changed (see Figure 6). Rather than override existing methods of TLabel to do this, we employ the same technique we used to modify the associate's behavior. Notice that instead of tapping into the WindowProc property, we override the WndProc method. How is this the same technique? If you look at TControl's constructor, you'll see that WindowProc is initialized to point at the WndProc method. So in essence, we are overriding the same method, but in a cleaner way, and without having to store the original value of WindowProc.

procedure TLinkedLabel.WndProc(var Message: TMessage);

begin

   if (Assigned(FAssociate) and (not FUpdating)) then begin

    FUpdating := True;

     try

       case(Message.Msg) of

        CM_ENABLEDCHANGED:    FAssociate.Enabled := Enabled;

        CM_VISIBLECHANGED:    FAssociate.Visible := Visible;

        WM_WINDOWPOSCHANGED: Adjust(False);

       end;

    finally

      FUpdating := False;

     end;

   end;

   inherited;

end;

Figure 6: Instead of tapping into the WindowProc property, we override the WndProc method.

One final point should be made about the previous component. You'll notice the use of FUpdating in both NewWinProc and WndProc. This variable is used to alert the LinkedLabel and the associate that the other component is making a change. If you don't do this, it's easy to create an infinite updating loop, or get unexpected results. Here's one flow of events that demonstrates the need for the FUpdating variable:

  • The user drags the LinkedLabel to a new position.
  • WndProc receives a WM_WINDOWPOSCHANGED message, and fires Adjust(False) to move the associate.
  • Adjust sets FAssociate.Left to the new value as part of repositioning the associate.
  • FAssociate fires off a WM_MOVE message, indicating it is has changed position.
  • NewWinProc detects the WM_MOVE message and calls Adjust(True) in an attempt to move the LinkedLabel to match the associate's new position.

As you can see, we haven't even gotten a chance to change the associate's Top property to match the LinkedLabel's new position before the associate tries to move the LinkedLabel. By using the FUpdating variable, the associate will not notice the WM_MOVE message and won't try to call Adjust to reposition the LinkedLabel.

A Couple of Issues

There are a couple of problems with the TLinkedLabel component that I did not address in this article. The following are brief descriptions:

  • You can cause all kinds of problems if you link two or more LinkedLabels to the same component, and then destroy one or more of them. You can end up breaking the link to other LinkedLabels, and even cause the linked component's WindowProc to point to a non-existent routine.
  • If you link a LinkedLabel to a component on a different form, the Notification method won't be called when that component is destroyed. Calling FreeNotification when the component is linked will fix this, but that doesn't really address the problem. The real problem is that we allowed it to be linked to the component on the other form in the first place. What we really want to do is restrict associates to only those components with the same parent as the LinkedLabel. Although it's not difficult to do this, it's a little tricky to only show eligible components in the Associate properties drop-down list in the Object Inspector.

Conclusion

That's about it. Replacing the WindowProc of an existing component does have its limitations, but can be a very useful technique. I can't think of any other reasonable way to design a component like TLinkedLabel and have the associate control move the LinkedLabel when the associate is moved. I'm not going to try and list other possible uses for this technique, because they are countless and limited only by a programmer's ingenuity.

All source referenced in this article is available for download.

Jeremy Merrill is an EDS contractor in a partnership contract with the Veteran's Health Administration. He is a member of the VA's Computerized Patient Record System development team, located in the Salt Lake City Chief Information Officer's Field Office.

Microsoft Internet Explorer

Top of page
 
1stClass Components

Informant Communications Group

Informant Communications Group, Inc.
10519 E. Stockton Blvd., Suite 100
Elk Grove, CA 95624-9703
Phone: (916) 686-6610 ò Fax: (916) 686-8497

Copyright ⌐ 2000 Informant Communications Group. All Rights Reserved. ò Site Use Agreement ò Send feedback to the Webmaster ò Important information about privacy